#!/usr/bin/env python
# -*- coding:utf-8 -*-


'''
PACKET SPECIFICATION:
    packet ::= route_slice separator data_slice
    separator ::= '\t'

    route_slice         ::= action_field service_field id_field

    action_field        ::= 'action:' action_name
    action_name         ::= 'REQUEST' | 'RESPONSE' | 'FORWARD'

    service_field       ::= 'service:' service_name
    service_name        ::= letter (letter | digit)*

    id_field            ::= packet_id_field [respone_id_field]
    packet_id_field     ::= 'packid:' decimalinteger
    response_id_field   ::= 'respid:' decimalinteger

    data_slice ::= 'body:' data
    data       ::= base64_encode_string

    identifier ::=  (letter|"_") (letter | digit | "_")*
    letter     ::=  lowercase | uppercase
    lowercase  ::=  "a"..."z"
    uppercase  ::=  "A"..."Z"
    digit      ::=  "0"..."9"

    decimalinteger ::=  nonzerodigit digit* | "0"
    nonzerodigit   ::=  "1"..."9"

'''

import os, sys, signal
import socket, threading, time
import base64

from jujuc import jujucore, jujuchat

def DEBUG(format, *args) : print format % args

#######################################################################

will_stop = False

def signal_stop( signo, frame ) :
    global  will_stop
    DEBUG( "JUJUB catch #%d signal, would stop.", signo )
    will_stop = True
    return
    # signal_stop


#######################################################################

TERMINATOR = '\n'

MIN_PACKET_ID = 1
MAX_PACKET_ID = 65535
packet_id_current = MAX_PACKET_ID

PP_ACTION_REQUEST  = 'REQUEST'
PP_ACTION_RESPONSE = 'RESPONSE'


class   Packet(object)  :
    def __init__(self) :
        self.action = None
        self.service = None
        self.body = None
        self.packid = 0
        self.respid = 0
        # __init__()

    def __str__(self) :
        s = "action:%s service:%s packid:%d" % (self.action, self.service, self.packid)
        if self.respid > 0 :
            s += " respid:%d" % self.respid
        s += " body:'%s'" % self.body
        return s
        # __str__()

    def isComplete(self) :
        if not self.action or not self.service or not self.body :
            return  False
        return  True

    def setRequest(self) :
        if PP_ACTION_REQUEST == self.action : return
        self.action = PP_ACTION_REQUEST
    def setResponse(self) :
        if PP_ACTION_RESPONSE == self.action : return
        self.action = PP_ACTION_RESPONSE

    def isRequest(self) :
        if PP_ACTION_REQUEST == self.action : return True
        return  False
    def isResponse(self) :
        if PP_ACTION_RESPONSE == self.action : return True
        return  False
    # class Packet

def depack(record) :
    '''decode packet record string into Packet struct'''
    field_separator = '\t'
    kv_separator = ':'
    packet = Packet()
    fields = record.split(field_separator)
    for f in fields :
        (key, value) = f.split(kv_separator, 1)
        if not value : continue
        if 'action' == key :
            packet.action = value
        elif 'service' == key :
            packet.service = value
        elif 'packid' == key :
            packet.packid = int(value)
        elif 'respid' == key :
            packet.respid = int(value)
        elif 'body' == key :
            packet.body = base64.b64decode(value)

    if not packet.isComplete() : return None
    return  packet
    # depack()

def enpack(packet) :
    '''encode Packet struct into packet record string'''
    packid = packet.packid
    if not packid :  packid = nextPacketID()
    record = "action:%s\tservice:%s\tpackid:%d" % (packet.action, packet.service, packid)
    if packet.respid > 0 :
        record += "\trespid:%d" % packet.respid
    record += "\tbody:%s" % base64.b64encode(packet.body)
    return  (record, packid)
    # enpack

def nextPacketID() :
    global  packet_id_current
    if packet_id_current == MAX_PACKET_ID :
        packet_id_current = MIN_PACKET_ID
    else :
        packet_id_current += 1
    return  packet_id_current
    # packetid()

#######################################################################

class   Context(object) :
    '''Context -- context for Consumer'''
    def __init__(self, dispatcher) :
        self.dispatcher = dispatcher
        # __init__()
    # class Context

#######################################################################

ACTION_TYPE_NONE = 0
ACTION_TYPE_SEND = 1
ACTION_TYPE_RECV = 2
ACTION_TYPE_SENDnRECV = 3

class   BaseAction(object) :
    def __init__(self, job=None) :
        self.action_type = ACTION_TYPE_NONE
        self.job = job
        self.remote_address = None
        self.remote_service = None

        self.data = None        # request
        self.result = None      # response
        self.reason = None      # failure
        # __init__()
    def execute(self) :
        '''execute action, and return status-tuple

        on success, return (True, data), data will go to done()
        on failure, retunr (False, reason), reason will go to fail()
        '''
        return True,None
        # execute()
    def done(self, data=None) :
        self.result = data
        # done()
    def fail(self, reason=None) :
        self.reason = reason
        # fail()
    def isSendAction(self) :
        return self.action_type == ACTION_TYPE_SEND
        # isSendAction()
    def isRecvAction(self) :
        return self.action_type == ACTION_TYPE_RECV
        # isRecvAction()
    def isSendRecvAction(self) :
        return self.action_type == ACTION_TYPE_SENDnRECV
        # isSendRecvAction()
    # class BaseAction

class   SendAction(BaseAction) :
    def __init__(self, job=None) :
        BaseAction.__init__(self, job)
        self.action_type = ACTION_TYPE_SEND
        # __init__()
    # class SendAction()

class   RecvAction(BaseAction) :
    def __init__(self, job=None) :
        BaseAction.__init__(self, job)
        self.action_type = ACTION_TYPE_RECV
        # __init__()
    # class SendAction()

class   SendRecvAction(BaseAction) :
    def __init__(self, job=None) :
        BaseAction.__init__(self, job)
        self.action_type = ACTION_TYPE_SENDnRECV
        # __init__()
    # class SendAction()


class   BaseJob(object) :
    def currentAction(self) :
        raise NotImplementedError
        # currentAction()

    def nextAction(self) :
        '''return next action
        if no more action, return None
        if no action at present, but job not end, return '' (scheduler would degrade priority of this job)
        '''
        raise NotImplementedError
        # nextAction()
    # class BaseJob

#######################################################################

class   Action4Channel(object) :
    def __init__(self) :
        self.q_send = []
        self.q_recv = []    # wait for any, including response
        self.q_recv_response = {}   # wait for response identified by respid

        self.q_in = jujuchat.fifo()      # incoming packet

        self.sending_packid = 0
        # __init__()

    def addSendAction(self, action) :
        self.q_send.append(action)
        # addSendJob()

    def addRecvAction(self, action) :
        self.q_recv.append(action)
        # addRecvJob()

    def addRecvResponseAction(self, action, respid) :
        if respid in self.q_recv_response :
            self.q_recv_response[respid].append(action)
        else :
            self.q_recv_response[respid] = [action,]
        # addRecvResponseJob()

    def popRecvActionFor(self, respid) :
        if respid in self.q_recv_response :
            q = self.q_recv_response[respid]
            action = q.pop(0)
            if len(q) < 1 :
                del self.q_recv_response[respid]
            return action
        return None
        # resposeFor()

    def popRecvAction(self) :
        if self.q_recv :
            return self.q_recv.pop(0)
        return None
        # popRecvAction()

    # class Job4Channel

class   Dispatcher(object)  :
    '''Dispatcher -- dispatch message to consumer'''
    def __init__(self) :
        # table-address : address --> channel
        self.__table_address = {}
        # table-channel : channel --> state of channel
        self.__table_channels = {}
        # table-consumer : SERVICE --> server
        self.__table_services = {}

        self.job_list_ready = []
        self.job_list_done =  []
        self.job_list_fail =  []
        self.job_list_run  =  []

        self.action_list_for_channel = {}
        # __init__()

    def forward(self, channel_from, record) :
        '''forward a record to a channel
        '''
        pass
        # forward()

    def dispatch(self, channel_from, record) :
        '''dispatch a record to a consumer

        List of Actions
         * parse record to get address info (such as SERVICE name)
         * lookup route-table to get the Consumer who is responsible for the record,
            if No Consumer is responsible for it :
                discard the message(record)
                return False
            OR
            if No Consumer is responsible for it :
                use Router as Consumer (route the message again)
         * feed message to consumer
         * return True
        '''
        # parse record, to get SERVICE
        DEBUG('CHANNEL %s RECV %s', channel_from.address, `record`)
        packet = depack(record)
        if not packet : raise RuntimeError, 'invalid record: %s' % record

        if packet.isRequest() :
            service = packet.service
            # find Consumer for the SERVICE
            if service not in self.__table_services : return  False
            #    for each SERVICE, organize Consumers as a list, we just use the first consumer
            server = self.__table_services[service][0]
            # just ignore, or log it to some where if NOT ok
            response, address_to = server.serve(service, packet.body, channel_from.address)
            if response is None :
                # No response
                return True
            packet.body = response
            packet.respid = packet.packid
            packet.packid = 0
            packet.setResponse()
            record, response_packid = enpack(packet)
            self.passon(record, address_to)
        elif packet.isResponse() :
            if channel_from not in self.action_list_for_channel :
                #DEBUG('action_list_for_channel : %s', str(self.action_list_for_channel))
                raise RuntimeError, 'CHANNEL %s do NOT exists (%s)' % (channel_from.address, `record`)

            actions = self.action_list_for_channel[ channel_from ]
            action = actions.popRecvActionFor( packet.respid )
            if action is None :
                #DEBUG('q_in PUSH %s', `packet`)
                actions.q_in.push( packet )
                return True
            #if action is None :
            #    raise RuntimeError, 'CHANNEL %s do NOT response for (%s)' % (channel_from.address, `record`)

            # action callback
            action.done( packet.body )
            # move job to done-list
            job = action.job
            self.job_list_done.append(job)
        else :
            raise RuntimeError, 'cannot handle %s action for record "%s"' % (packet.action, `record`)

        return  True
        # dispatch()

    def passon(self, record, address) :
        '''pass packet on a channel by address

        todo : what to do if no channal registered for address???
        '''
        channel = self.lookupChannel(address)
        if channel is None :
            raise RuntimeError, 'No channel labeled %s' % address
        channel.push(record + TERMINATOR)
        # passon()

    ###################################################################

    def registerChannel(self, address, channel) :
        self.__table_address[ address ] = channel
        # registerChannel()
    def unregisterChannel(self, address) :
        if not address : return
        if address in self.__table_address :
            channel = self.__table_address[ address ]
            del self.__table_address[ address ]
        # unregisterChannel()
    def lookupChannel(self, address) :
        if address in self.__table_address :
            return self.__table_address[ address ]
        return None
        # lookupChannel()

    ###################################################################

    def addJob(self, job) :
        self.job_list_done.append(job)
        # addJob()

    def scheduleReadyJobs(self) :
        count_job = len(self.job_list_ready)
        if count_job < 1 :
            # no ready jobs
            return
        while count_job > 0 :
            count_job -= 1
            job = self.job_list_ready.pop(0)
            current_action = job.currentAction()
            if current_action.isSendAction() :
                address = current_action.remote_address
                channel = self.lookupChannel(address)
                if channel is None :
                    raise RuntimeError, 'No channel for %s' % address
                if channel not in self.action_list_for_channel :
                    self.action_list_for_channel[ channel ] = Action4Channel()
                self.action_list_for_channel[ channel ].addSendAction( current_action )
                self.job_list_run.append(job)
            elif current_action.isRecvAction() :
                address = current_action.remote_address
                channel = self.lookupChannel(address)
                if channel is None :
                    raise RuntimeError, 'No channel for %s' % address
                if channel not in self.action_list_for_channel :
                    self.action_list_for_channel[ channel ] = Action4Channel()
                self.action_list_for_channel[ channel ].addRecvAction( current_action )
                self.job_list_run.append(job)
            elif current_action.isSendRecvAction() :
                address = current_action.remote_address
                channel = self.lookupChannel(address)
                if channel is None :
                    raise RuntimeError, 'No channel for %s' % address
                if channel not in self.action_list_for_channel :
                    self.action_list_for_channel[ channel ] = Action4Channel()
                self.action_list_for_channel[ channel ].addSendAction( current_action )
                self.job_list_run.append(job)
            else :
                ok,crump = job.execute()
                if ok :
                    job.done(crump)
                    self.job_list_done.append(job)
                else :
                    job.fail(crump)
                    self.job_list_fail.append(job)
        # scheduleReadyJob()

    def scheduleDoneJobs(self) :
        count_job = len(self.job_list_done)
        if count_job < 1 :
            # no done jobs
            return
        while count_job > 0 :
            count_job -= 1
            job = self.job_list_done.pop(0)
            action = job.nextAction()
            if action :
                self.job_list_ready.append(job)
            elif action is None :
                # no more action
                del job
            else :
                self.job_list_done.append(job)
        # scheduleDoneJob()

    def scheduleFailJobs(self) :
        count_job = len(self.job_list_fail)
        if count_job < 1 :
            # no failure jobs
            return

        while count_job > 0 :
            count_job -= 1
            job = self.job_list_fail.pop(0)
            del job
        # scheduleFailJob()

    def loop_once(self) :
        self.scheduleFailJobs()
        self.scheduleDoneJobs()
        self.scheduleReadyJobs()

        self.scheduleConsumers()
        self.scheduleProducers()
        # loop_once()

    def countJobs(self) :
        count_ready = len(self.job_list_ready)
        count_done  = len(self.job_list_done)
        count_fail  = len(self.job_list_fail)

        return count_ready + count_done + count_fail
        # countJobs()

    def countRunningJobs(self) :
        return len(self.job_list_run) + len(self.job_list_ready)
        # countRunningJobs()

    def scheduleConsumers(self) :
        for channel, actions in self.action_list_for_channel.iteritems() :
            length_q_in = len(actions.q_in)
            if length_q_in < 1 :
                continue

            for i in xrange(length_q_in) :
                one_zero, packet = actions.q_in.pop()
                #DEBUG('q_in POP %s', `packet`)
                if packet.isResponse() :
                    action = actions.popRecvActionFor( packet.respid )
                    if action is None :
                        #DEBUG('q_in PUSH %s', `packet`)
                        actions.q_in.push( packet )
                        continue
                    #if action is None :
                    #    raise RuntimeError, 'CHANNEL %s do NOT response for (%s)' % (channel_from.address, `record`)

                    # action callback
                    action.done( packet.body )
                    # move job to done-list
                    job = action.job
                    self.job_list_done.append(job)
        # scheduleConsumers()

    def scheduleProducers(self) :
        #DEBUG('action_list_for_channel : %s', str(self.action_list_for_channel))
        for channel, actions in self.action_list_for_channel.iteritems() :
            if channel.countProducers() > 0 :
                continue
            if len(actions.q_send) < 1 :
                continue
            action = actions.q_send[0]
            packet = Packet()
            packet.setRequest()
            packet.body = action.data
            packet.service = action.remote_service
            record, packid = enpack(packet)
            DEBUG('CHANNEL %s PUSH (packid=%d size=%d)%s', channel.address, packid, len(record), `record`)
            actions.sending_packid = packid
            #DEBUG('CHANNEL %s sending packet (id:%d)', channel, actions.sending_packid)
            channel.push(record + TERMINATOR)
        # scheduleProducers()

    def OnPostSend(self, channel) :
        if channel not in self.action_list_for_channel :
            raise RuntimeError, 'Suspicious Channel %s' % channel
        actions = self.action_list_for_channel[ channel ]
        action = actions.q_send.pop(0)
        job = action.job
        if job is None :
            # a standalone action
            return
        if action.isSendAction() :
            # schedule job to done-list
            self.job_list_done.append(job)
        elif action.isSendRecvAction() :
            # schedule action to recv Queue
            # register respid to wait
            #DEBUG('CHANNEL %s sending packet (id:%d)', channel, actions.sending_packid)
            DEBUG('Action RecvResponse for %d', actions.sending_packid )
            actions.addRecvResponseAction(action, actions.sending_packid)
            actions.sending_packid = 0

        # OnPostSend()

    # class Dispatcher

#######################################################################

class   JuChannel(jujuchat.Jujuchat) :
    def    __init__(self, address, context, connect=None) :
        jujuchat.Jujuchat.__init__(self, conn=connect)
        if connect :
            self.__connect = connect
        else :
            self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
            self.connect(address)
            self.__connect = self.socket
        self.address = address
        self.__buf_incoming = []
        self.__terminator = TERMINATOR
        self.setTerminator(self.__terminator)

        self.__context = context
        self.__context.dispatcher.registerChannel(self.address, self)
        # __init__()

    def OnRecvRecord(self, record):
        self.__context.dispatcher.dispatch(self, record)
        # OnRecvRecord()
    def OnPostSendRecord(self) :
        DEBUG('JUJUB CHANNEL %s SEND-DONE.', self.address)
        self.__context.dispatcher.OnPostSend(self)
        # OnPostSendRecord()

    def handle_connect(self) :
        pass
        # handle_connect()

    def handle_close(self) :
        #DEBUG('JUJUB close connection for client "%s".', self.address)
        self.__context.dispatcher.unregisterChannel(self.address)
        self.close()
        # handle_close()

    # class JuChannel

#######################################################################

class   Jujube(object) :
    def __init__(self) :
        dispatcher = Dispatcher()
        context = Context(dispatcher)
        self.__context = context
        # __init__()

    def connect(self, address) :
        JuChannel(address, self.__context)
        # connect()

    def addJob(self, job) :
        self.__context.dispatcher.addJob(job)
        # addJob()

    def run(self) :
        # mask signal
        #DEBUG('os.name: %s', os.name)
        signal.signal( signal.SIGTERM, signal_stop )
        if 'posix' == os.name :
            signal.signal( signal.SIGQUIT, signal_stop )

        try :
            while True :
                if self.__context.dispatcher.countRunningJobs() < 1 :
                    if len(jujucore.socket_map) < 1 : break
                jujucore.loop(timeout=1.0, count=1)
                self.__context.dispatcher.loop_once()
        except KeyboardInterrupt :
            DEBUG( "JUJUB interrupt : %s", `sys.exc_info()[1]` )
            pass
        except :
            raise
        # run()
    # class Jujube

#######################################################################

SERVICE = 'ECHO'

class   ClientJob(BaseJob) :
    def __init__(self, peer_address, times=10) :
        self.peer_address = peer_address
        self.times = times
        self.count = 0
        self.action = None
        # __init__

    def currentAction(self) :
        return self.action
        # currentAction()

    def nextAction(self) :
        if self.count >= self.times :
            return None
        self.count += 1
        action = SendRecvAction(job=self)
        msg = msg = '#' + str(self.count) + ' hello, world.'
        action.data = msg
        action.remote_service = SERVICE
        action.remote_address = self.peer_address
        self.action = action
        return action
        # nextAction()
    # class ClientJob

HOST = "127.0.0.1"
PORT = 54344

def main():
    address = (HOST, PORT)
    c = Jujube()
    c.connect(address)

    job = ClientJob(address)
    c.addJob(job)

    c.run()
    # main()

if __name__ == '__main__' :
    main()
    import sys
    sys.exit(0)
